Skip to content

✨ feat: Support MCP Elicitation (URL + Form) for Deferred Auth in Tool Calls#71

Draft
TomasPalsson wants to merge 10 commits into
apro-deployfrom
feat/mcp-url-elicitation
Draft

✨ feat: Support MCP Elicitation (URL + Form) for Deferred Auth in Tool Calls#71
TomasPalsson wants to merge 10 commits into
apro-deployfrom
feat/mcp-url-elicitation

Conversation

@TomasPalsson

Copy link
Copy Markdown

Summary

This adds client + server support for MCP elicitation — the mechanism by which an MCP server pauses a tool call to ask the user for something before it can proceed. LibreChat now handles both wire mechanisms from the 2025-11-25 MCP spec:

  • URL mode — a tools/call returns the JSON-RPC error -32042 (UrlElicitationRequired), or the server sends an elicitation/create request with mode: "url". LibreChat surfaces an in-chat authorization card; the user opens the server-provided URL (e.g. a browser OAuth flow) and, once they confirm, the original tools/call is retried automatically — exactly once.
  • Form mode — an elicitation/create request carrying a requestedSchema. LibreChat renders a validated form (string / number / boolean / enum) and returns the user's accept / decline / cancel response to the server.

Motivation / root cause. MCP servers that front delegated auth — notably AWS Bedrock AgentCore Gateway, which defers per-user OAuth to a browser flow — signal "the user has to authorize in a browser first" via -32042. Today LibreChat treats that as a hard tool failure, so those servers are unusable through the gateway. This change makes the deferral a first-class, resumable interaction instead of an error.

Targets apro-deploy. This supersedes #69 (same work, but with the branch on the org repo); #69 will be closed. No upstream issue is linked because this lands on our internal integration branch, not danny-avila/LibreChat.

Change Type

  • New feature (non-breaking change which adds functionality)

Testing

Automated — all green locally:

  • packages/api/src/mcp/__tests__/elicitation.test.ts (29) — -32042 extraction across JSON-RPC / SDK McpError / HTTP-wrapped transport shapes, http(s)-only URL allow-listing (rejects javascript:/data:/vbscript:), the per-connection handler registry, and the abort-signal path.
  • packages/api/src/mcp/__tests__/MCPManager.test.ts (62) — single-retry semantics, the concurrent-pending cap, timeout tie-off, and requestedSchema flow-metadata threading.
  • api/server/routes/__tests__/mcp.spec.js + api/server/services/__tests__/MCP.spec.js (145) — the POST /api/mcp/elicitation/:flowId authorization matrix (401 / 400 bad-action / 400 schema-violation / 403 cross-user / 404 / 200), the on_elicitation / on_elicitation_resolved SSE emitters, and the config gate.
  • client/.../__tests__/ElicitationForm.test.tsx (19) + client/src/hooks/SSE/__tests__/useStepHandler.spec.ts (65) — card rendering, the unsafe-URL and NaN-guard paths, localized validation, and the resolution write-back handler.
  • packages/api/src/mcp/__tests__/urlElicitation.integration.test.ts (12) — end-to-end retry against a real MCP SDK stack (local only; excluded from CI by the repo's *integration* ignore pattern).

Reproduce:

npm run test:packages:api            # packages/api unit suites
cd api    && NODE_ENV=test npx jest server/routes/__tests__/mcp.spec.js server/services/__tests__/MCP.spec.js
cd client && NODE_ENV=test npx jest src/components/Chat/Messages/Content/__tests__/ElicitationForm.test.tsx src/hooks/SSE/__tests__/useStepHandler.spec.ts

tsc --noEmit is clean on packages/api and client; ESLint/Prettier clean on all touched files.

Manual — verified end-to-end against an AWS Bedrock AgentCore Gateway target: the card renders, the auth link opens, the tool call auto-retries and succeeds after confirmation, and a declined/cancelled flow surfaces a clean error with no retry loop.

Test Configuration:

  • Node 26.4.0, macOS (Darwin 25.4). CI runs the sharded backend-review / frontend-review / eslint-ci jobs.
  • MCP server: AWS Bedrock AgentCore Gateway (delegated OAuth), plus a local stub returning -32042 (packages/api/src/mcp/__tests__/helpers/).

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • I have made pertinent documentation changes — the new elicitation server flag is documented in librechat.example.yaml; the external librechat.ai docs are not updated (internal branch).
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

Notes for reviewers

  • Config gate. Elicitation is gated per-server by an elicitation flag (in the MCP server config schema, documented in librechat.example.yaml), enabled by default; set elicitation: false on a server to disable it — no code change or restart-with-revert needed.
  • Security. Server-supplied auth URLs are validated to http(s) only, both server-side (extraction) and client-side (render), so a compromised MCP server can't inject javascript:/data: URIs. The completion route is requireJwtAuth + per-user flow-ownership checked (unguessable UUID flowId), and user-submitted form content is validated server-side against the requestedSchema before it reaches the server — client-side validation is not trusted.
  • Concurrency. A per-connection handler registry behind one stable dispatcher ensures concurrent tool calls sharing a cached connection can't orphan or misattribute each other's elicitation/create. Pending elicitations are capped per (user, server).
  • Known limitation — full-reload persistence. A resolved card survives live re-render and SSE resumable-replay (via on_elicitation_resolved), but is not re-materialized after a full page reload, because the canonical content aggregator lives in the @librechat/agents package (out of scope for this PR). This matches the existing behavior of OAuth sign-in cards. Deferred intentionally.
  • CI coverage caveat. The a11y and general *integration* jobs are gated to the danny-avila/LibreChat upstream repo / excluded by pattern, so they don't execute on this fork PR. Accessibility (persistent sr-only aria-live region, focus handling) was reviewed manually against the sibling tool-call cards.

Surface MCP server-initiated elicitation during tool calls. When a
tools/call returns JSON-RPC -32042 (UrlElicitationRequired, spec
2025-11-25) or the server sends elicitation/create, LibreChat holds the
call open, prompts the user, and resolves/retries once they respond.
Primary driver: AWS Bedrock AgentCore Gateway, which defers per-user
OAuth to a browser flow and signals it via -32042.

Backend:
- Detect and extract -32042 from JSON-RPC error objects, SDK McpError,
  and HTTP-wrapped transport errors; accept only http(s) auth URLs
  (reject javascript:/data:/vbscript:) in both wire shapes.
- Per-connection elicitation handler registry behind one stable
  dispatcher, so concurrent tool calls on a shared connection cannot
  orphan or misattribute each other's elicitation/create.
- Cap concurrent pending elicitations per (user, server); tie the
  tool-call timeout to the flow TTL; thread the SDK abort signal so a
  server cancellation or connection close tears down the pending wait.
- Gate per-server via an `elicitation` config flag (enabled by default);
  validate submitted content against the requested schema server-side;
  strip elicitation parts from the model-bound payload.
- Emit on_elicitation and on_elicitation_resolved SSE events; add
  data-provider types, endpoints, and the completion route with
  per-user flow-ownership authorization.

Covered by unit and integration tests for detection, single-retry
semantics, the handler registry, the config gate, and the route authz
matrix.
Render an in-chat card for MCP elicitation: an authorization link for URL
mode and a validated form (string/number/boolean/enum) for form mode,
each with accept/decline/cancel. On the -32042 flow the user opens the
server-provided auth URL and the tool call auto-retries after they
confirm.

- Render only http(s) auth URLs; show a warning instead of a link for
  javascript:/data:/vbscript: and keep Continue disabled.
- Localize every validation message via useLocalize with interpolation;
  guard numeric fields against NaN and omit empty optional fields.
- Announce resolved status through a persistent sr-only aria-live region,
  matching the sibling tool-call cards.
- Consume the on_elicitation_resolved SSE event and patch the resolved
  card in place so it survives re-render.

Covered by ElicitationForm and useStepHandler unit tests, including the
unsafe-URL and NaN-guard paths.
Close the remaining gaps against the authoritative MCP 2025-11-25
elicitation spec (form + URL mode), verified by an audit of the spec's
client-side MUST/SHOULD clauses.

URL-mode client security (client/ElicitationForm):
- Show the full authorization URL as visible text for examination before
  the user consents to open it (spec MUST), not just an "Open" button.
- Highlight the destination host/domain, matching the existing OAuth
  sign-in card treatment (subdomain-spoofing mitigation).
- Warn on Punycode / mixed-script (homograph) hostnames.

Form-mode requestedSchema subset — render + validate the full set:
- string `pattern` (regex) and `format` (email/uri/date/date-time,
  mapped to native input types);
- `oneOf: [{const, title}]` titled enums and `enumNames` display titles;
- multi-select `type: "array"` (items enum/anyOf, minItems/maxItems),
  emitted as a real JSON array in the response payload.

Server-side (routes/mcp.js) validates the same subset — pattern, format,
oneOf membership, and array membership/bounds — so client validation
cannot be bypassed by a direct API call.

Also retain the server-supplied `elicitationId` alongside the flow id
(kept internal) instead of discarding it.

Known follow-ups (documented, not addressed here): negotiated
protocol-version gating for URL mode, a -32602 override for malformed
mode values (currently the SDK default -32603), and a
notifications/elicitation/complete auto-retry handler (spec MAY).

Covered by new client and server tests (email/uri/date formats, pattern,
oneOf, multi-select array, full-URL display, domain + Punycode warning,
and the server validation matrix).
Backport of the review fixes from the upstream URL-mode slice
(danny-avila#14141) to the full implementation:

- Gate the advertised elicitation capability on the per-server
  `elicitation` flag, so an opted-out server (`elicitation: false`)
  isn't told the client supports elicitation.
- Apply the per-(user, server) MAX_PENDING_ELICITATIONS cap to the
  -32042 URL-exception path too (previously only the elicitation/create
  path was capped), preventing unbounded concurrent flows.
- Give stripUiOnlyContentParts type-honest overloads so the non-array
  pass-through isn't dead code under the T[] signature.
- Correct the elicitationFlowContext eviction comment to match the
  size-bounded, TTL-swept-when-over-cap behavior.
Once an elicitation resolves, render a muted single-line confirmation at
the sibling tool call's "Completed …" altitude instead of a persistent
full-width card. Mirrors the in-ToolCall OAuth sign-in, which leaves no
lingering confirmation once the call proceeds; kept (not removed) so a
cancel/decline stays legible and the transcript keeps a breadcrumb.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b8bf158-c0e0-423b-b6be-8435af4bab2a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-url-elicitation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

- Refuse URL elicitation on the shared app-level connection, so a gateway
  that binds auth to the MCP session can't leave it authorized as one user
  for everyone else (Codex P1).
- Reject the loser of a concurrent completion: completeFlow returns true even
  for an already-completed flow, so re-read and 409 if another action won the
  race, instead of emitting a second resolution (Codex P3).
Pending URL-mode elicitation cards are UI-only events, so they were never
reconstructed on resume — a refresh during a pending authorization lost the
card until the flow timed out. Persist on_elicitation/on_elicitation_resolved
via the same replay-event path OAuth cards use (deduped by flowId), so a
resumed client re-renders the card and can still complete or cancel it.

(cherry picked from commit f50b363)
…ction

The app-level connection guard now refuses URL elicitation on the shared
app connection, so these tests must model the user-scoped connection the
real flow uses (empty app connections + getUserConnection).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant